/*
 * Copyright (C) 2011 Lavettacn Xiao (lavettacn@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.lavettacn.android.app.sociallessness.data.table;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.util.ArrayList;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

public abstract class BaseTable {

    public static final int TYPE_STRING = 0;
    public static final int TYPE_BOOLEAN = 1;
    public static final int TYPE_SHORT = 2;
    public static final int TYPE_INT = 3;
    public static final int TYPE_LONG = 4;
    public static final int TYPE_FLOAT = 5;
    public static final int TYPE_DOUBLE = 6;
    public static final int TYPE_BLOB = 7;
    public static final String SQLITE_TYPES[] =
        { "TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "REAL", "REAL", "NONE" };

    private final ColumnInfo[] mColumnInfo;
    private String mTableName;
    private final String[] mProjection;

    public BaseTable() {
        mTableName = parseTableName();
        ColumnInfo[] columns = parseColumnInfo();
        mColumnInfo = columns;
        String[] projection = {};
        if (columns != null) {
            projection = new String[columns.length];
            for (int i = 0; i != columns.length; ++i) {
                ColumnInfo column = columns[i];
                projection[i] = column.name;
            }
        }
        mProjection = projection;
    }

    @Column(TableElement.ID)
    public long mId = 0;

    @Column(TableElement.USER_NAME)
    public String mUserName = null;

    @Column(TableElement.USER_ID)
    public long mUid = 0;

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface Table {
        String value();
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface Column {
        String value();
    }

    public static final class ColumnInfo {
        public final String name;
        public final int type;
        public final String extraSql = "";
        public final Field field;
        public final int projectionIndex;

        public ColumnInfo(String name, int type, Field field, int projectionIndex) {
            this.name = name.toLowerCase();
            this.type = type;
            this.field = field;
            this.projectionIndex = projectionIndex;
        }

        public boolean isId() {
            return TableElement.ID.equals(name);
        }
    }

    public final void createTable(SQLiteDatabase db) {
        String tableName = mTableName;
        if (tableName == null) {
            return;
        }

        StringBuilder sql = new StringBuilder("CREATE TABLE ");
        sql.append(tableName);
        sql.append(" (_id INTEGER PRIMARY KEY");
        ColumnInfo[] columns = mColumnInfo;
        int numColumns = columns.length;
        for (int i = 0; i != numColumns; ++i) {
            ColumnInfo column = columns[i];
            if (!column.isId()) {
                sql.append(',');
                sql.append(column.name);
                sql.append(' ');
                sql.append(SQLITE_TYPES[column.type]);
                if (column.extraSql != null) {
                    sql.append(' ');
                    sql.append(column.extraSql);
                }
            }
        }
        sql.append(");");
        db.execSQL(sql.toString());
        sql.setLength(0);
    }

    public final void dropTable(SQLiteDatabase db) {
        String tableName = mTableName;
        StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS ");
        sql.append(tableName);
        sql.append(';');
        db.execSQL(sql.toString());
        sql.setLength(0);
    }

    public final void deleteAll(SQLiteDatabase db) {
        StringBuilder sql = new StringBuilder("DELETE FROM ");
        sql.append(mTableName);
        sql.append(";");
        db.execSQL(sql.toString());
    }

    public final boolean deleteWithId(SQLiteDatabase db, long id) {
        return db.delete(mTableName, TableElement.ID + "=?",
                new String[] { Long.toString(id) }) == 1;
    }

    public String getTableName() {
        return mTableName;
    }

    public long insertOrReplace(SQLiteDatabase db) {
        ContentValues values = new ContentValues();
        objectToValues(this, values);
        if (mId == 0) {
            values.remove(TableElement.ID);
        }
        long id = db.replace(mTableName, TableElement.ID, values);
        mId = id;
        return id;
    }

    public String[] getProjection() {
        return mProjection;
    }

    public void loadFromCursor(Cursor cursor) {
        try {
            ColumnInfo[] columns = mColumnInfo;
            for (int i = 0, size = columns.length; i != size; ++i) {
                ColumnInfo column = columns[i];
                int columnIndex = column.projectionIndex;
                Field field = column.field;
                switch (column.type) {
                case TYPE_STRING:
                    field.set(this, cursor.getString(columnIndex));
                    break;
                case TYPE_BOOLEAN:
                    field.setBoolean(this, cursor.getShort(columnIndex) == 1);
                    break;
                case TYPE_SHORT:
                    field.setShort(this, cursor.getShort(columnIndex));
                    break;
                case TYPE_INT:
                    field.setInt(this, cursor.getInt(columnIndex));
                    break;
                case TYPE_LONG:
                    field.setLong(this, cursor.getLong(columnIndex));
                    break;
                case TYPE_FLOAT:
                    field.setFloat(this, cursor.getFloat(columnIndex));
                    break;
                case TYPE_DOUBLE:
                    field.setDouble(this, cursor.getDouble(columnIndex));
                    break;
                case TYPE_BLOB:
                    field.set(this, cursor.getBlob(columnIndex));
                    break;
                }
            }
        } catch (IllegalArgumentException e) {
        } catch (IllegalAccessException e) {
        }
    }

    private String parseTableName() {
        Table table = getClass().getAnnotation(Table.class);
        if (table == null) {
            return null;
        }
        return table.value();
    }

    private ColumnInfo[] parseColumnInfo() {
        ArrayList<ColumnInfo> columns = new ArrayList<ColumnInfo>();
        Field[] fields = getClass().getFields();
        for (int i = 0; i != fields.length; ++i) {
            Field field = fields[i];
            Column info = ((AnnotatedElement) field).getAnnotation(Column.class);
            if (info == null) {
                continue;
            }

            int type;
            Class<?> fieldType = field.getType();
            if (fieldType == String.class) {
                type = TYPE_STRING;
            } else if (fieldType == boolean.class) {
                type = TYPE_BOOLEAN;
            } else if (fieldType == short.class) {
                type = TYPE_SHORT;
            } else if (fieldType == int.class) {
                type = TYPE_INT;
            } else if (fieldType == long.class) {
                type = TYPE_LONG;
            } else if (fieldType == float.class) {
                type = TYPE_FLOAT;
            } else if (fieldType == double.class) {
                type = TYPE_DOUBLE;
            } else if (fieldType == byte[].class) {
                type = TYPE_BLOB;
            } else {
                throw new IllegalArgumentException("Unsupported field type for column: " + fieldType.getName());
            }

            int index = columns.size();
            columns.add(new ColumnInfo(info.value(), type, field, index));
        }

        // Return a list.
        ColumnInfo[] columnList = new ColumnInfo[columns.size()];
        columns.toArray(columnList);
        return columnList;
    }

    private void objectToValues(BaseTable object, ContentValues values) {
        try {
            ColumnInfo[] columns = mColumnInfo;
            for (int i = 0, size = columns.length; i != size; ++i) {
                ColumnInfo column = columns[i];
                String columnName = column.name;
                Field field = column.field;
                switch (column.type) {
                case TYPE_STRING:
                    values.put(columnName, (String) field.get(object));
                    break;
                case TYPE_BOOLEAN:
                    values.put(columnName, field.getBoolean(object));
                    break;
                case TYPE_SHORT:
                    values.put(columnName, field.getShort(object));
                    break;
                case TYPE_INT:
                    values.put(columnName, field.getInt(object));
                    break;
                case TYPE_LONG:
                    values.put(columnName, field.getLong(object));
                    break;
                case TYPE_FLOAT:
                    values.put(columnName, field.getFloat(object));
                    break;
                case TYPE_DOUBLE:
                    values.put(columnName, field.getDouble(object));
                    break;
                case TYPE_BLOB:
                    values.put(columnName, (byte[]) field.get(object));
                    break;
                }
            }
        } catch (IllegalArgumentException e) {
        } catch (IllegalAccessException e) {
        }
    }

}
